﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace BlackStar
{
    public class MemoContentReader : ContentTypeReader<Memo>
    {
        protected override Memo Read(ContentReader input, Memo existingInstance)
        {
            Memo result = new Memo();
            result.ReadContent(input);
            return result;
        }
    }

    public class Memo : Component
    {
        //protected VScrollbar myScrollbar = new VScrollbar();

        //protected Texture2D textCache;
        Rectangle textBoxArea = Rectangle.Empty;
        Rectangle textArea = Rectangle.Empty;
        List<String> AllLines = new List<string>();
        VScrollbar myScrollbar = new VScrollbar();

        protected string displayText = string.Empty;
        protected List<String> DisplayLines = new List<string>();

        protected int mySelectStart = 0;
        protected int mySelectEnd = 0;
        protected bool selecting = false;

        protected bool cursorBlink = true;
        protected int cursorTime = 0;

        protected float lineHeight = 0;

        protected int ScrollSleep = 0;

        protected bool myLocked = true;
        protected Color myHighlightBack = Color.Navy;
        protected Color myHighlightFore = Color.White;
        
        public Memo()
            : base()
        {
            myScrollbar.WriteToContent = false;
            //myScrollbar.Visible = true;
            myScrollbar.Value = myScrollbar.Maximum = myScrollbar.Minimum = 0;
            myScrollbar.Name = "InternalScroll";
            myScrollbar.Parent = this;
            myScrollbar.ValueChanged += new ComponentEvent(myScrollbar_ValueChanged);
            Children.Add(myScrollbar);
        }

        [ContentSerializer(Optional = true)]
        public Color HighlightForeColor
        {
            get { return myHighlightFore; }
            set { myHighlightFore = value; }
        }

        [ContentSerializer(Optional = true)]
        public Color HighlightBackColor
        {
            get { return myHighlightBack; }
            set { myHighlightBack = value; }
        }

        [ContentSerializer(Optional = true)]
        public bool Locked
        {
            get { return myLocked; }
            set { myLocked = value; }
        }

        [ContentSerializer(Optional = true)]
        public int SelectEnd
        {
            get { return mySelectEnd; }
            set { mySelectEnd = value; ScrollToCursor(); }
        }

        [ContentSerializer(Optional = true)]
        public int SelectStart
        {
            get { return mySelectStart; }
            set { mySelectStart = value; }
        }


        void myScrollbar_ValueChanged(Component sender)
        {
            if (myGui == null) return;
            displayText = DisplayText();
        }

        public override void Resize()
        {
            if (layoutSuspended) return;
            UpdateRectangles();

            // now my draw rectangle has been updated, so we need to update the scroll bar.
            int scrollw = myGui.theme.Scrollbar.Rectangles["ScrollUp"].Width;
            FindComponent("InternalScroll").Rectangle =
                new Rectangle(myDrawRectangle.Width - scrollw, 0, scrollw, myDrawRectangle.Height);

            textBoxArea = myDrawRectangle;
            textBoxArea.Width -= scrollw;

            textArea = textBoxArea;
            textArea.X += myPadding.X;
            textArea.Y += myPadding.Y;
            textArea.Width -= (myPadding.X + myPadding.Width);
            textArea.Height -= (myPadding.Y + myPadding.Height);

            foreach (Component c in Children) { c.Resize(); }
            OnResize();
        }

        protected override void SetText(string value)
        {
            base.SetText(value);
            if (myGui == null) return;
            UpdateAllLines();

            displayText = DisplayText();

        }

        public override void Draw()
        {
            if (!myVisible) return;

            if (myText != string.Empty && displayText == string.Empty) SetText(myText);

            myGui.theme.Textbox.Draw("Default", textBoxArea, this.Color);

            myGui.theme.Fonts[this.Font].Draw(displayText, textArea, this.FontSize, this.FontColor);

            int cursorPos;
            int cursorLine;
            GetLineFromPos(mySelectEnd, out cursorLine, out cursorPos);
            cursorLine -= myScrollbar.Value;

            int startPos;
            int startLine;
            GetLineFromPos(Math.Min(mySelectStart, mySelectEnd), out startLine, out startPos);
            startLine -= myScrollbar.Value;

            int endPos;
            int endLine;
            GetLineFromPos(Math.Max(mySelectEnd, mySelectStart), out endLine, out endPos);
            endLine -= myScrollbar.Value;

            Rectangle lineRect = textArea;
            lineRect.Height = this.FontSize;

            if (DisplayLines.Count == 0 && this.HasFocus() && (cursorBlink || cursorTime > 0))
            { gui.theme.Fonts[this.Font].DrawCursor(endPos, "", lineRect, this.FontColor, this.FontSize); }
            else
            {
                for (int i = 0; i < DisplayLines.Count; i++)
                {
                    int sp = startPos;
                    int ep = endPos;

                    if (myText.Trim() != string.Empty)
                    {

                        if (i == startLine)
                        {

                            if (startLine != endLine) ep = DisplayLines[i].Length;
                            Vector2 startV = myGui.theme.Fonts[this.Font].MeasureString(this.FontSize, DisplayLines[i].Substring(0, sp));
                            Vector2 endV = myGui.theme.Fonts[this.Font].MeasureString(this.FontSize, DisplayLines[i].Substring(0, ep));
                            string highlightText = DisplayLines[i].Substring(Math.Min(sp, ep), Math.Abs(sp - ep));

                            Rectangle highlightArea = new Rectangle(lineRect.X + (int)Math.Ceiling(Math.Min(startV.X, endV.X)),
                                                                    lineRect.Y,
                                                                    (int)Math.Ceiling(Math.Abs((endV.X - startV.X))),
                                                                    (int)Math.Max(endV.Y, startV.Y));
                            highlightArea.Width -= Math.Max(highlightArea.Right - lineRect.Right, 0);
                            gui.DrawRectangle(highlightArea, myHighlightBack, true);
                            highlightArea.Width += 1;
                            gui.theme.Fonts[this.Font].Draw(highlightText, highlightArea, this.FontSize, myHighlightFore);
                        }
                        else if (i == endLine)
                        {
                            if (startLine != endLine) sp = 0;
                            Vector2 startV = myGui.theme.Fonts[this.Font].MeasureString(this.FontSize, DisplayLines[i].Substring(0, sp));
                            Vector2 endV = myGui.theme.Fonts[this.Font].MeasureString(this.FontSize, DisplayLines[i].Substring(0, ep));
                            string highlightText = DisplayLines[i].Substring(Math.Min(sp, ep), Math.Abs(sp - ep));

                            Rectangle highlightArea = new Rectangle(lineRect.X + (int)Math.Ceiling(Math.Min(startV.X, endV.X)),
                                                                    lineRect.Y,
                                                                    (int)Math.Ceiling(Math.Abs((endV.X - startV.X))),
                                                                    (int)Math.Max(endV.Y, startV.Y));
                            highlightArea.Width -= Math.Max(highlightArea.Right - lineRect.Right, 0);
                            gui.DrawRectangle(highlightArea, myHighlightBack, true);
                            highlightArea.Width += 1;
                            gui.theme.Fonts[this.Font].Draw(highlightText, highlightArea, this.FontSize, myHighlightFore);
                        }
                        else if (i > startLine && i < endLine)
                        {
                            sp = 0;
                            ep = DisplayLines[i].Length;

                            Vector2 startV = myGui.theme.Fonts[this.Font].MeasureString(this.FontSize, DisplayLines[i].Substring(0, sp));
                            Vector2 endV = myGui.theme.Fonts[this.Font].MeasureString(this.FontSize, DisplayLines[i].Substring(0, ep));
                            string highlightText = DisplayLines[i].Substring(Math.Min(sp, ep), Math.Abs(sp - ep));

                            Rectangle highlightArea = new Rectangle(lineRect.X + (int)Math.Ceiling(Math.Min(startV.X, endV.X)),
                                                                    lineRect.Y,
                                                                    (int)Math.Ceiling(Math.Abs((endV.X - startV.X))),
                                                                    (int)Math.Max(endV.Y, startV.Y));
                            highlightArea.Width -= Math.Max(highlightArea.Right - lineRect.Right, 0);
                            gui.DrawRectangle(highlightArea, myHighlightBack, true);
                            highlightArea.Width += 1;
                            gui.theme.Fonts[this.Font].Draw(highlightText, highlightArea, this.FontSize, myHighlightFore);
                        }

                    }

                    if (i == cursorLine && this.HasFocus() && (cursorBlink || cursorTime > 0))
                    {
                        gui.theme.Fonts[this.Font].DrawCursor(cursorPos, DisplayLines[i], lineRect, this.FontColor, this.FontSize);
                    }

                    lineRect.Y = textArea.Y + (int)(lineHeight * (i + 1));
                }
            }
            DrawComponents();
        }

        protected void UpdateAllLines()
        {
            Font myFont = myGui.theme.Fonts[this.Font];
            int mySize = this.FontSize;
            Vector2 measure = Vector2.Zero;

            string remains = myText;
            string currentChar;

            string currentLine = string.Empty;
            string nextLine = string.Empty;
            AllLines.Clear();

            while (remains.Length > 0)
            {


                currentChar = remains.Substring(0, 1);
                remains = remains.Substring(1, remains.Length - 1);

                if (currentChar == "\n")
                {
                    AllLines.Add(currentLine);
                    currentLine = string.Empty;
                    continue;
                }

                measure = myFont.MeasureString(mySize, currentLine + currentChar);
                // someday someone will complain about these 5 pixels.
                // and I will tell them take it out of their build.
                // don't come crying to me when you see weird line wraps.
                if (measure.X <= textArea.Width - 5)
                { currentLine += currentChar; nextLine = string.Empty; }
                else
                {
                    // the current line will no longer fit if we add the next char, so we need to 
                    // cut back to the last white space. 
                    int whiteIndex = currentLine.LastIndexOfAny(new char[] { '\n', '\t', ' ' });
                    if (whiteIndex == -1)
                    {
                        // there is no white space.
                        nextLine = currentChar;
                    }
                    else
                    {
                        // we found whitepace. everything after the found white space becomes nextline,
                        // everything befor the found white space becomes the current line.
                        nextLine = currentLine.Substring(whiteIndex + 1) + currentChar;
                        currentLine = currentLine.Substring(0, whiteIndex);
                    }
                    // current line is now at its max, so add it to the display lines.
                    AllLines.Add(currentLine);
                    currentLine = nextLine;
                }
            }
            // add the last remaining bit.
            AllLines.Add(currentLine);
        }

        protected string DisplayText()
        {
            if (myText.Trim() == string.Empty) { return string.Empty; }

            int lineCount = (int)Math.Floor((double)textArea.Height / (double)this.FontSize);

            //((Scrollbar)FindComponent("InternalScroll")).Maximum = Math.Max(AllLines.Count - lineCount, 0);
            myScrollbar.Maximum = Math.Max(AllLines.Count - lineCount, 0);

            //int startline = ((Scrollbar)FindComponent("InternalScroll")).Value;
            int startline = myScrollbar.Value;

            //List<String> VisibleLines = new List<string>();

            DisplayLines.Clear();
            for (int i = startline; i < (lineCount + startline) && i < AllLines.Count; i++)
            { DisplayLines.Add(AllLines[i]); }

            string result = ListStringToString(DisplayLines);

            lineHeight = myGui.theme.Fonts[this.Font].MeasureString(FontSize, result).Y / DisplayLines.Count;

            return result;
        }

        protected string ListStringToString(List<String> data)
        {
            if (data.Count < 1) return string.Empty;
            string result = data[0];
            for (int i = 1; i < data.Count; i++)
            { result += "\n" + data[i]; }
            return result;
        }

        protected int GetTextPos(Point p)
        {
            if (AllLines.Count < 1) return 0;

            // figure out where in the text we are. 
            // first what line was clicked.
            int line = (int)Math.Floor((double)(p.Y - textArea.Y) / lineHeight);
            line = Math.Max(line, 0);
            // we add the scrollbar value to account for line numbers that aren't displayed.
            line += myScrollbar.Value;

            int charPos = 0;
            if (line > AllLines.Count - 1)
            {
                // the line can't be larger than the last index in our list of lines.
                line = Math.Min(AllLines.Count - 1, line);
                charPos = AllLines[line].Length;
            }
            else
            {
                // find the character the mouse is nearest.
                if (AllLines[line].Trim() == string.Empty)
                { charPos = 0; }
                else
                { charPos = myGui.theme.Fonts[this.Font].CursorPos(AllLines[line], p, textArea, this.FontSize); }
            }

            // now translate line number and character position into a position in the base text.
            return GetTextPos(line, charPos);
        }

        protected int GetTextPos(int line, int charPos)
        {
            int result = 0;
            for (int i = 0; i < AllLines.Count && i < line; i++)
            { result += AllLines[i].Length + 1; }
            result += charPos;
            return result;
        }

        public override void DoMouseDown(MouseEventArgs e)
        {
            TakeFocus();
            if (e.Buttons.Left)
            {
                this.mySelectStart = this.mySelectEnd = GetTextPos(e.Location);
                selecting = true;
            }
        }

        protected void GetLineFromPos(int pos, out int line, out int charPos)
        {
            int aPos = pos;
            line = 0;

            for (int i = 0; i < AllLines.Count; i++)
            {
                if (aPos >= AllLines[i].Length + 1)
                {
                    line++;
                    aPos -= (AllLines[i].Length + 1);
                    //charPos = 0;
                }
                else
                {
                    break;
                }
            }
            charPos = aPos;
        }

        public override void Update(GameTime gameTime)
        {
            if (ScrollSleep > 0)
            {
                ScrollSleep -= gameTime.ElapsedRealTime.Milliseconds;
            }

            // if the left button is no longer down, stop selecting.
            if (!gui.mouse.LeftButton) { selecting = false; }

            this.cursorBlink = ((gameTime.TotalRealTime.Milliseconds % 1000) >= 500);
            cursorTime = Math.Max(0, cursorTime - gameTime.ElapsedGameTime.Milliseconds);

            if (selecting && gui.mouse.LeftButton)
            {
                if ((gui.mouse.Location.Y < textArea.Y) && ScrollSleep <= 0)
                {
                    myScrollbar.Value -= 1;
                    ScrollSleep = 250;
                }

                if (gui.mouse.Location.Y > (textArea.Y + (DisplayLines.Count * lineHeight)) && ScrollSleep <= 0)
                {
                    myScrollbar.Value += 1;
                    ScrollSleep = 250;
                }

                mySelectEnd = GetTextPos(gui.mouse.Location);
                mySelectEnd = Math.Max(mySelectEnd, 0);
                cursorTime = 500;
            }

            base.Update(gameTime);
        }

        public override void DoKeyDown(KeyEventArgs e)
        {
            string newText = myText;

            int linepos = 0;
            int curspos = 0;

            switch (e.Key)
            {
                case Keys.Tab:
                    if (myGui.keyboard.Shift)
                    { DoTabPrev(); }
                    else
                    { DoTabNext(); }
                    break;
                case Keys.Home:
                    if (myGui.keyboard.Control)
                    {
                        mySelectEnd = 0;
                        myScrollbar.Value = myScrollbar.Minimum;
                    }
                    else
                    {
                        // move to the begining of the current line.

                        GetLineFromPos(mySelectEnd, out linepos, out curspos);
                        curspos = 0;
                        mySelectEnd = GetTextPos(linepos, curspos);
                    }
                    if (!myGui.keyboard.Shift) mySelectStart = mySelectEnd;
                    break;
                case Keys.End:
                    if (myGui.keyboard.Control)
                    {
                        mySelectEnd = myText.Length;
                    }
                    else
                    {
                        // move to the end of the current line.
                        GetLineFromPos(mySelectEnd, out linepos, out curspos);
                        curspos = AllLines[linepos].Length;
                        mySelectEnd = GetTextPos(linepos, curspos);
                    }
                    if (!myGui.keyboard.Shift) mySelectStart = mySelectEnd;
                    break;
                case Keys.Left:
                    mySelectEnd -= 1;
                    mySelectEnd = Math.Max(mySelectEnd, 0);
                    if (!myGui.keyboard.Shift) { mySelectStart = mySelectEnd; }
                    break;
                case Keys.Right:
                    mySelectEnd += 1;
                    mySelectEnd = Math.Min(mySelectEnd, myText.Length);
                    if (!myGui.keyboard.Shift) { mySelectStart = mySelectEnd; }
                    break;
                case Keys.PageDown:
                    GetLineFromPos(mySelectEnd, out linepos, out curspos);
                    linepos = Math.Min(linepos + DisplayLines.Count, AllLines.Count -1);
                    myScrollbar.Value += DisplayLines.Count;
                    curspos = Math.Min(AllLines[linepos].Length, curspos);
                    mySelectEnd = GetTextPos(linepos, curspos);
                    if (!myGui.keyboard.Shift) mySelectStart = mySelectEnd;
                    break;
                case Keys.PageUp:
                    GetLineFromPos(mySelectEnd, out linepos, out curspos);
                    linepos = Math.Max(linepos - DisplayLines.Count, 0);
                    myScrollbar.Value -= DisplayLines.Count;
                    curspos = Math.Min(AllLines[linepos].Length, curspos);
                    mySelectEnd = GetTextPos(linepos, curspos);
                    if (!myGui.keyboard.Shift) mySelectStart = mySelectEnd;
                    break;
                case Keys.Up:
                    GetLineFromPos(mySelectEnd, out linepos, out curspos);
                    linepos = Math.Max(linepos - 1, 0);
                    // this is a cheat for the moment. It would be better to 
                    // measure the width of the text and find the closest match on the 
                    // new line. but for now, it will do.
                    curspos = Math.Min(AllLines[linepos].Length, curspos);
                    mySelectEnd = GetTextPos(linepos, curspos);
                    if (!myGui.keyboard.Shift) mySelectStart = mySelectEnd;
                    break;
                case Keys.Down:
                    GetLineFromPos(mySelectEnd, out linepos, out curspos);
                    linepos = Math.Min(linepos + 1, AllLines.Count - 1);
                    // this is a cheat for the moment. see case Key.Up
                    curspos = Math.Min(AllLines[linepos].Length, curspos);
                    mySelectEnd = GetTextPos(linepos, curspos);
                    if (!myGui.keyboard.Shift) mySelectStart = mySelectEnd;
                    break;
                case Keys.Back:
                    if (myLocked) break;
                    if (mySelectEnd != mySelectStart)
                    {
                        newText = myText.Remove(Math.Min(mySelectStart, mySelectEnd), Math.Abs(mySelectEnd - mySelectStart));
                        mySelectStart = mySelectEnd = Math.Min(mySelectStart, mySelectEnd);
                    }
                    else if (mySelectStart > 0)
                    {
                        newText = myText.Remove(mySelectStart - 1, 1);
                        mySelectStart = mySelectEnd -= 1;
                    }
                    break;
                case Keys.Delete:
                    if (myLocked) break;
                    if (mySelectEnd != mySelectStart)
                    {
                        newText = myText.Remove(Math.Min(mySelectStart, mySelectEnd), Math.Abs(mySelectEnd - mySelectStart));
                        mySelectStart = mySelectEnd = Math.Min(mySelectStart, mySelectEnd);
                    }
                    else
                    {
                        if (mySelectStart < myText.Length)
                        { newText = Text.Remove(mySelectStart, 1); }
                    }
                    break;
                default:
                    // convert it to keystroke?
                    if (myGui.keyboard.Control)
                    {
                        switch (e.Key)
                        {
                            case Keys.C:
                                // copy
                                myGui.clipboard.Text = this.SelectedText();
                                break;
                            case Keys.V:
                                // paste
                                if (myLocked) break;
                                newText = myText.Remove(Math.Min(mySelectStart, mySelectEnd), Math.Abs(mySelectEnd - mySelectStart));
                                mySelectStart = mySelectEnd = Math.Min(mySelectStart, mySelectEnd);
                                string cbtext = myGui.clipboard.Text;
                                newText = newText.Insert(mySelectEnd, cbtext);
                                mySelectStart = mySelectEnd += cbtext.Length;
                                break;
                            case Keys.X:
                                // cut
                                myGui.clipboard.Text = this.SelectedText();
                                if (myLocked) break;
                                newText = myText.Remove(Math.Min(mySelectStart, mySelectEnd), Math.Abs(mySelectEnd - mySelectStart));
                                mySelectStart = mySelectEnd = Math.Min(mySelectStart, mySelectEnd);
                                break;
                            case Keys.A:
                                // select all
                                mySelectStart = 0;
                                mySelectEnd = myText.Length;
                                break;
                        }
                    }
                    else
                    {
                        if (myLocked) break;
                        string aKey = myGui.keyboard.GetChar(e.Key);
                        if (e.Key == Keys.Enter) aKey = "\r\n";
                        if (aKey == "") break;
                        newText = myText.Remove(Math.Min(mySelectStart, mySelectEnd), Math.Abs(mySelectEnd - mySelectStart));
                        mySelectStart = mySelectEnd = Math.Min(mySelectStart, mySelectEnd);
                        newText = newText.Insert(mySelectEnd, aKey);
                        mySelectStart = mySelectEnd += 1;
                        if (e.Key == Keys.Enter) mySelectStart = mySelectEnd += 1;
                    }
                    break;
            }
            cursorTime = 500;

            this.Text = newText;

            ScrollToCursor();

            OnKeyDown(e);
        }

        public string SelectedText()
        {
            int start = Math.Min(mySelectStart, mySelectEnd);
            int len = Math.Max(mySelectStart, mySelectEnd) - start;
            return myText.Substring(start, len); 
        }

        private void ScrollToCursor()
        {
            int cursorpos = 0;
            int cursorline = 0;
            GetLineFromPos(mySelectEnd, out cursorline, out cursorpos);
            //System.Diagnostics.Debug.WriteLine(cursorline.ToString() + ":" + cursorpos.ToString());
            while (cursorline > DisplayLines.Count - 1 && myScrollbar.Value < myScrollbar.Maximum)
            {
                myScrollbar.Value += 1;
                GetLineFromPos(mySelectEnd, out cursorline, out cursorpos);
            }

            while (cursorline < myScrollbar.Value && myScrollbar.Value > myScrollbar.Minimum)
            { myScrollbar.Value -= 1; }
        }

        public override void DoMouseScroll(MouseEventArgs e)
        {
            myScrollbar.Value += e.ScrollChange;
            OnMouseScroll();
        }
    }
}
